// Take a look at the license at the top of the repository in the LICENSE file.

use crate::{DiskUsage, Gid, Pid, Process, ProcessRefreshKind, ProcessStatus, Signal, Uid};

use std::ffi::{OsStr, OsString};
use std::fmt;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::ptr::NonNull;

use super::ffi;
use super::utils::{WrapMap, c_buf_to_os_string, from_cstr_array, get_sys_value_osstr};

impl fmt::Display for ProcessStatus {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(match *self {
            ProcessStatus::Idle => "Idle",
            ProcessStatus::Run => "Runnable",
            ProcessStatus::Sleep => "Sleeping",
            ProcessStatus::Stop => "Stopped",
            ProcessStatus::Zombie => "Zombie",
            ProcessStatus::Dead => "Dead",
            ProcessStatus::LockBlocked => "LockBlocked",
            _ => "Unknown",
        })
    }
}

pub(crate) struct ProcessInner {
    pub(crate) name: OsString,
    pub(crate) cmd: Vec<OsString>,
    pub(crate) exe: Option<PathBuf>,
    pub(crate) pid: Pid,
    parent: Option<Pid>,
    pub(crate) environ: Vec<OsString>,
    pub(crate) cwd: Option<PathBuf>,
    pub(crate) root: Option<PathBuf>,
    pub(crate) memory: u64,
    pub(crate) virtual_memory: u64,
    pub(crate) updated: bool,
    cpu_usage: f32,
    start_time: u64,
    run_time: u64,
    pub(crate) status: ProcessStatus,
    user_id: Uid,
    effective_user_id: Uid,
    group_id: Gid,
    effective_group_id: Gid,
    read_bytes: u64,
    old_read_bytes: u64,
    written_bytes: u64,
    old_written_bytes: u64,
    accumulated_cpu_time: u64,
    exists: bool,
}

impl ProcessInner {
    pub(crate) fn kill_with(&self, signal: Signal) -> Option<bool> {
        let c_signal = crate::sys::convert_signal(signal)?;
        unsafe { Some(libc::kill(self.pid.0, c_signal) == 0) }
    }

    pub(crate) fn name(&self) -> &OsStr {
        &self.name
    }

    pub(crate) fn cmd(&self) -> &[OsString] {
        &self.cmd
    }

    pub(crate) fn exe(&self) -> Option<&Path> {
        self.exe.as_deref()
    }

    pub(crate) fn pid(&self) -> Pid {
        self.pid
    }

    pub(crate) fn environ(&self) -> &[OsString] {
        &self.environ
    }

    pub(crate) fn cwd(&self) -> Option<&Path> {
        self.cwd.as_deref()
    }

    pub(crate) fn root(&self) -> Option<&Path> {
        self.root.as_deref()
    }

    pub(crate) fn memory(&self) -> u64 {
        self.memory
    }

    pub(crate) fn virtual_memory(&self) -> u64 {
        self.virtual_memory
    }

    pub(crate) fn parent(&self) -> Option<Pid> {
        self.parent
    }

    pub(crate) fn status(&self) -> ProcessStatus {
        self.status
    }

    pub(crate) fn start_time(&self) -> u64 {
        self.start_time
    }

    pub(crate) fn run_time(&self) -> u64 {
        self.run_time
    }

    pub(crate) fn cpu_usage(&self) -> f32 {
        self.cpu_usage
    }

    pub(crate) fn accumulated_cpu_time(&self) -> u64 {
        self.accumulated_cpu_time
    }

    pub(crate) fn disk_usage(&self) -> DiskUsage {
        DiskUsage {
            written_bytes: 0,       // self.written_bytes.saturating_sub(self.old_written_bytes),
            total_written_bytes: 0, // self.written_bytes,
            read_bytes: 0,          // self.read_bytes.saturating_sub(self.old_read_bytes),
            total_read_bytes: 0,    // self.read_bytes,
        }
    }

    pub(crate) fn user_id(&self) -> Option<&Uid> {
        Some(&self.user_id)
    }

    pub(crate) fn effective_user_id(&self) -> Option<&Uid> {
        Some(&self.effective_user_id)
    }

    pub(crate) fn group_id(&self) -> Option<Gid> {
        Some(self.group_id)
    }

    pub(crate) fn effective_group_id(&self) -> Option<Gid> {
        Some(self.effective_group_id)
    }

    pub(crate) fn wait(&self) -> Option<ExitStatus> {
        crate::unix::utils::wait_process(self.pid)
    }

    pub(crate) fn session_id(&self) -> Option<Pid> {
        unsafe {
            let session_id = libc::getsid(self.pid.0);
            if session_id < 0 {
                None
            } else {
                Some(Pid(session_id))
            }
        }
    }

    pub(crate) fn switch_updated(&mut self) -> bool {
        std::mem::replace(&mut self.updated, false)
    }

    pub(crate) fn set_nonexistent(&mut self) {
        self.exists = false;
    }

    pub(crate) fn exists(&self) -> bool {
        self.exists
    }

    pub(crate) fn open_files(&self) -> Option<usize> {
        // FIXME
        None
    }

    pub(crate) fn open_files_limit(&self) -> Option<usize> {
        crate::System::open_files_limit()
    }
}

#[inline]
fn get_accumulated_cpu_time(kproc: &libc::kinfo_proc2) -> u64 {
    // from htop source code
    100 * (kproc.p_rtime_sec as u64 + ((kproc.p_rtime_usec as u64 + 500_000) / 1_000_000))
}

fn get_active_status(kd: NonNull<ffi::kvm_t>, kproc: &libc::kinfo_proc2) -> Option<ProcessStatus> {
    let mut nlwps = 0;
    unsafe {
        let klwps = ffi::kvm_getlwps(
            kd.as_ptr(),
            kproc.p_pid,
            kproc.p_paddr,
            std::mem::size_of::<libc::kinfo_lwp>(),
            &mut nlwps,
        );
        if klwps.is_null() || nlwps < 1 {
            return None;
        }
        let klwps: &[libc::kinfo_lwp] = std::slice::from_raw_parts(klwps, nlwps as _);
        for entry in klwps {
            match entry.l_stat {
                ffi::LSONPROC => return Some(ProcessStatus::Run),
                ffi::LSRUN => return Some(ProcessStatus::Suspended),
                ffi::LSSLEEP => return Some(ProcessStatus::Sleep),
                ffi::LSSTOP => return Some(ProcessStatus::Stop),
                _ => {}
            }
        }
    }
    None
}

fn update_proc_info(
    kproc: &libc::kinfo_proc2,
    system_info: &super::system::SystemInfo,
    now: u64,
    refresh_kind: ProcessRefreshKind,
    proc_: &mut ProcessInner,
) {
    proc_.run_time = now.saturating_sub(proc_.start_time);

    if refresh_kind.cwd().needs_update(|| proc_.cwd.is_none()) {
        unsafe {
            proc_.cwd = get_path(proc_.pid, ffi::KERN_PROC_CWD);
        }
    }

    if refresh_kind.exe().needs_update(|| proc_.exe.is_none()) {
        unsafe {
            proc_.exe = get_path(proc_.pid, ffi::KERN_PROC_PATHNAME);
        }
    }

    if refresh_kind.disk_usage() {
        proc_.old_read_bytes = proc_.read_bytes;
        proc_.read_bytes = kproc.p_uru_inblock as _;
        proc_.old_written_bytes = proc_.written_bytes;
        proc_.written_bytes = kproc.p_uru_oublock as _;
    }

    if refresh_kind.cpu() {
        proc_.accumulated_cpu_time = get_accumulated_cpu_time(kproc);
    }

    let cmd_needs_update = refresh_kind.cmd().needs_update(|| proc_.cmd.is_empty());
    if proc_.name.is_empty() || cmd_needs_update {
        let cmd =
            unsafe { from_cstr_array(ffi::kvm_getargv2(system_info.kd.as_ptr(), kproc, 0) as _) };

        if !cmd.is_empty() {
            // First, we try to retrieve the name from the command line.
            let p = Path::new(&cmd[0]);
            if let Some(name) = p.file_name() {
                name.clone_into(&mut proc_.name);
            }

            if cmd_needs_update {
                proc_.cmd = cmd;
            }
        }
        if proc_.name.is_empty() {
            // The name can be cut short because the `ki_comm` field size is limited,
            // which is why we prefer to get the name from the command line as much as
            // possible.
            proc_.name = c_buf_to_os_string(&kproc.p_comm);
        }
    }

    if refresh_kind
        .environ()
        .needs_update(|| proc_.environ.is_empty())
    {
        proc_.environ =
            unsafe { from_cstr_array(ffi::kvm_getenvv2(system_info.kd.as_ptr(), kproc, 0) as _) };
    }

    // We now get the values needed for both new and existing process.
    if refresh_kind.cpu() {
        proc_.cpu_usage = (100 * kproc.p_pctcpu) as f32 / system_info.fscale;
    }

    // Processes can be reparented apparently?
    proc_.parent = if kproc.p_ppid != 0 {
        Some(Pid(kproc.p_ppid))
    } else {
        None
    };

    proc_.status = match kproc.p_realstat {
        ffi::SIDL => ProcessStatus::Idle,
        ffi::SSTOP => ProcessStatus::Stop,
        ffi::SZOMB => ProcessStatus::Zombie,
        ffi::SDEAD => ProcessStatus::Tracing,
        ffi::SACTIVE => get_active_status(system_info.kd, kproc)
            .unwrap_or(ProcessStatus::Unknown(kproc.p_realstat as _)),
        _ => ProcessStatus::Unknown(kproc.p_realstat as _),
    };

    if refresh_kind.memory() {
        proc_.virtual_memory = kproc.p_vm_vsize as _;
        proc_.memory = (kproc.p_vm_rssize as u64).saturating_mul(system_info.page_size as _);
    }

    proc_.updated = true;
}

pub(crate) unsafe fn get_process_data(
    kproc: &libc::kinfo_proc2,
    wrap: &WrapMap,
    now: u64,
    refresh_kind: ProcessRefreshKind,
    system_info: &super::system::SystemInfo,
) -> Result<Option<Process>, ()> {
    if kproc.p_flag & (ffi::P_SYSTEM as i32) != 0 {
        // We filter out the kernel threads.
        return Err(());
    }

    // FIXME: This is to get the "real" run time (in micro-seconds).
    // let run_time = (kproc.ki_runtime + 5_000) / 10_000;

    let start_time = kproc.p_ustart_sec as u64;

    if let Some(proc_) = unsafe { (*wrap.0.get()).get_mut(&Pid(kproc.p_pid)) } {
        let proc_ = &mut proc_.inner;
        proc_.updated = true;
        // If the `start_time` we just got is different from the one stored, it means it's not the
        // same process.
        if proc_.start_time == start_time {
            update_proc_info(kproc, system_info, now, refresh_kind, proc_);

            return Ok(None);
        }
    }

    // This is a new process, we need to get more information!

    let mut inner = ProcessInner {
        pid: Pid(kproc.p_pid),
        parent: None,
        user_id: Uid(kproc.p_ruid),
        effective_user_id: Uid(kproc.p_uid),
        group_id: Gid(kproc.p_rgid),
        effective_group_id: Gid(kproc.p_gid),
        start_time,
        run_time: 0,
        cpu_usage: 0.,
        virtual_memory: 0,
        memory: 0,
        cwd: None,
        exe: None,
        name: OsString::new(),
        cmd: Vec::new(),
        root: None,
        environ: Vec::new(),
        status: ProcessStatus::Unknown(0),
        read_bytes: 0,
        old_read_bytes: 0,
        written_bytes: 0,
        old_written_bytes: 0,
        accumulated_cpu_time: 0,
        updated: true,
        exists: true,
    };

    update_proc_info(kproc, system_info, now, refresh_kind, &mut inner);

    Ok(Some(Process { inner }))
}

pub(crate) unsafe fn get_path(pid: crate::Pid, kind: libc::c_int) -> Option<PathBuf> {
    let mut buffer = [0; libc::MAXPATHLEN as usize + 1];

    unsafe {
        get_sys_value_osstr(
            &[libc::CTL_KERN, libc::KERN_PROC_ARGS, pid.0, kind],
            &mut buffer,
        )
        .map(PathBuf::from)
    }
}
